#
# openlookup/engine/singleton.py
#
# Copyright 2008-2009 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
A single-valued mapping from principal and label to value

>>> db = SingletonEngine(':memory:', create=True)
>>> db.get('color') is None
True

>>> db.put('color', 'red', ttl=10)
>>> db.get('color')
'red'
>>> item = db.get_item('color')
>>> 9 <= item.ttl <= 10
True

>>> db.put('color', 'green', ttl=5)
>>> db.get('color')
'green'
>>> item = db.get_item('color')
>>> 9 <= item.ttl <= 11
True

>>> db.rm('color')
>>> db.get('color') is None
True

>>> from openlookup.engine.identity import generate
>>> p1 = generate(size=512)
>>> p2 = generate(size=512)
>>> db.put('color', 'red')
>>> db.put('color', 'green', p1)
>>> db.put('color', 'blue', p2)
>>> db.get('color')
'red'
>>> db.get('color', p1.hexdigest())
'green'
>>> db.get('color', p2.hexdigest())
'blue'
>>> sorted(db.query('color', maxvals=3))
['blue', 'green', 'red']
>>> db.rm('color', p2)
>>> sorted(db.query('color', maxvals=3))
['green', 'red']
>>> len(db.query('color', maxvals=1))
1
"""

from os import urandom
from hashlib import sha1
from binascii import hexlify

from openlookup.engine.item import MAX_TTL, create_singleton_item, hex_complement
from openlookup.engine.db import DB

MAX_MAXVALS = 20

class SingletonEngine(object):
    def __init__(self, filename, create=False, fast=False):
        self.db = DB(filename, create=create, fast=fast)

    def __enter__(self): return self
    def __exit__(self, *exc_info): self.close()
    def close(self): self.db.close()

    def put(self, label, value, identity=None, ttl=None):
        """
        Set the value for a label.
        """
        item = create_singleton_item(label, value, identity, ttl)
        self.put_item(item)

    def rm(self, label, identity=None, ttl=None):
        """
        Clear the value for a label.
        """
        item = create_singleton_item(label, None, identity, ttl)
        self.put_item(item)

    def get(self, label, principal=None):
        """
        Get the value for a label.
        """
        item = self.get_item(label, principal)
        if item is not None:
            return item.decrypt_value(label)
        else:
            return None

    def query(self, label, principal='', maxvals=MAX_MAXVALS):
        """
        Return some values for a label.

        The optional parameter "principal" can be used to specify a
        prefix of 0-40 hexadecimal digits to narrow the query to a
        subset of the principals.  An empty prefix (the default) also
        matches an unsigned entry.

        Distinct values are returned in a deterministic pseudo-random
        circular order, starting from a randomly chosen value (but not
        guaranteeing that all values are equally likely to be chosen).
        """
        return [item.decrypt_value(label)
                for item in self.query_items(label, principal, maxvals)]

    def put_item(self, item):
        """
        Store an item.

        If the item is not signed, the expiration time is adjusted if
        necessary to supersede a preexisting item.
        """
        item.validate()
        if item.get_ttl() > MAX_TTL + 5 * 60:
            raise ValueError('Expiration time is too far in the future.')

        self.db.update(item)

    def get_item(self, label, principal=None):
        """
        Look up an item.

        Note that the returned item can be a tombstone.  If no item is
        found, None is returned.

        A time-to-live attribute is added to the returned item.  This
        will never be non-negative.
        """
        label_hash = sha1(label).hexdigest()
        return self.get_item_by_label_hash(label_hash, principal)

    def get_item_by_label_hash(self, label_hash, principal=None):
        assert len(label_hash) == 40
        assert principal is None or len(principal) == 40

        key = label_hash
        if principal is not None:
            key += hex_complement(principal)

        item = self.db.get(key)
        if item is None or item.type != 'singleton':
            return None

        item.ttl = item.get_ttl()
        if item.ttl < 0:
            return None

        return item

    def query_items(self, label, principal='', maxvals=MAX_MAXVALS):
        """
        Return some non-tombstone items for a label.

        The optional parameter "principal" can be used to specify a
        prefix of 0-40 hexadecimal digits to narrow the query to a
        subset of the principals.  An empty prefix (the default) also
        matches an unsigned entry.

        Distinct items are returned in a deterministic pseudo-random
        circular order, starting from a randomly chosen item (but not
        guaranteeing that all items are equally likely to be chosen).
        """
        label_hash = sha1(label).hexdigest()
        return self.query_items_by_label_hash(label_hash, principal, maxvals)

    def query_items_by_label_hash(self, label_hash,
                                  principal='', maxvals=MAX_MAXVALS):
        maxvals = max(1, min(MAX_MAXVALS, maxvals))

        assert len(label_hash) == 40
        key = label_hash + hex_complement(principal)

        items = []
        for item in self.gen_items_by_key(key):
            items.append(item)
            if len(items) >= maxvals:
                break

        return items

    def gen_items_by_key(self, key):
        anchor = mark = hexlify(urandom(10))
        for item in self.db.query(key, mark, anchor):
            if item.type == 'singleton':
                item.ttl = item.get_ttl()
                if item.ttl >= 0:
                    yield item

def create_key(label, principal=None):
    key = sha1(label).hexdigest()
    if principal is not None:
        key += hex_complement(principal)
    return key

if __name__ == '__main__':
    import doctest
    doctest.testmod()
